﻿#region NeoPatterns
// 
// NeoPatterns framework is released under LGPLv3 license.
// See http://www.opensource.org/licenses/lgpl-3.0.html for more details.
// 
// Please visit http://neopatterns.wikidot.com for more information about NeoPatterns
// 
// Copyright (c) Pascal Craponne 2008
// 
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using NeoPatterns.Core.Registry;
using NeoPatterns.Utility;

namespace NeoPatterns.Setup.MetaAssembly
{
    public class MetaAssemblyLoader
    {
        private class ObjectDefinition
        {
            public Delegate Creator;
            public Delegate Initializer;
            public bool Instance;
            public Type Type;
        }

        private readonly IDictionary<string, ObjectDefinition> ObjectDefinitions;

        private MethodInfo StartupMethod;

        public static void Run()
        {
            new MetaAssemblyLoader().Execute();
        }

        [DebuggerStepThrough]
        private MetaAssemblyLoader()
        {
            ObjectDefinitions = new Dictionary<string, ObjectDefinition>();
        }

        private ObjectDefinition GetObjectDefinition(string name, Type type)
        {
            ObjectDefinition objectDefinition;
            if (!ObjectDefinitions.TryGetValue(name, out objectDefinition))
            {
                objectDefinition = new ObjectDefinition { Type = type };
                ObjectDefinitions[name] = objectDefinition;
            }
            return objectDefinition;
        }

        private void Execute()
        {
            var assemblies = GetAssemblies();
            ParseAssemblies(assemblies);
            RegisterObjects();
            // in non program assemblies (like services or web apps),
            // we have no startup method
            if (StartupMethod != null)
                StartupMethod.Invoke(null, new object[0]);
        }

        private void RegisterObjects()
        {
            foreach (var name in ObjectDefinitions.Keys)
            {
                var objectDefinition = ObjectDefinitions[name];
                MetaConfiguration.ObjectRegistry.RegisterDefinition(name, objectDefinition.Type, null,
                                                                 objectDefinition.Instance, objectDefinition.Creator, objectDefinition.Initializer);
            }
        }

        private void ParseAssemblies(IEnumerable<Assembly> assemblies)
        {
            foreach (var assembly in assemblies)
            {
                foreach (var type in assembly.GetTypes())
                {
                    var metaClassAttribute = type.GetCustomAttribute<MetaClassAttribute>(true);
                    if (metaClassAttribute != null)
                    {
                        foreach (var methodInfo in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
                        {
                            var metaCtor = methodInfo.GetCustomAttribute<MetaCreatorAttribute>(true);
                            if (metaCtor != null)
                            {
                                var objectDefinition = GetObjectDefinition(metaCtor.Name, methodInfo.ReturnType);
                                MethodInfo ctorMethod = methodInfo;
                                var ctor = (Func<IObjectRegistry, object>)(arg =>
                                                                               {
                                                                                   var instance = ctorMethod.Invoke(
                                                                                       null, new object[0]);
                                                                                   return instance;
                                                                               });
                                objectDefinition.Creator = ctor;
                                objectDefinition.Instance = objectDefinition.Instance || metaCtor.Instance;
                            }
                            var metaInitializer = methodInfo.GetCustomAttribute<MetaInitializerAttribute>(true);
                            if (metaInitializer != null)
                            {
                                var objectDefinition = GetObjectDefinition(metaInitializer.Name, methodInfo.GetParameters()[0].ParameterType);
                                MethodInfo initMethod = methodInfo;

                                // from here, we try to guess what the initializer wants
                                var methodParameters = initMethod.GetParameters();
                                Action<object, IObjectRegistry> initializer = null;
                                if (methodParameters.Length == 1)
                                {
                                    initializer = ((target, objectRegistry) => initMethod.Invoke(null, new[] { target }));
                                }
                                else if (methodParameters.Length == 2)
                                {
                                    if (methodParameters[0].ParameterType.IsAssignableFrom(typeof(IObjectRegistry)))
                                    {
                                        initializer = ((target, objectRegistry) => initMethod.Invoke(null, new[] { objectRegistry, target }));
                                    }
                                    else
                                    {
                                        initializer = ((target, objectRegistry) => initMethod.Invoke(null, new[] { target, objectRegistry }));
                                    }
                                }
                                if (initializer == null)
                                    throw new NotImplementedException(); // TODO: a real exception
                                objectDefinition.Initializer = initializer;
                                objectDefinition.Instance = objectDefinition.Instance || metaInitializer.Instance;
                            }
                            var metaEntryAttribute = methodInfo.GetCustomAttribute<MetaEntryAttribute>(true);
                            if (metaEntryAttribute != null)
                            {
                                StartupMethod = methodInfo;
                            }
                        }
                    }
                }
            }
        }

        private static List<Assembly> GetAssemblies()
        {
            var assemblies = new List<Assembly>();
            // for web applications, there is no entry assembly
            var entryAssembly = Assembly.GetEntryAssembly();
            if (entryAssembly != null)
                assemblies.Add(entryAssembly);
            // then parse all assemblies in domain
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                if (assembly.GetCustomAttributes(typeof(MetaAssemblyAttribute), true).Length > 0)
                    assemblies.Add(assembly);
            }
            return assemblies;
        }
    }
}